# 小智 AI Client Architecture Plan > 文档:小智 AI 硬件开放应用平台 —— 总体架构与开发 Plan > Version: V1.1 > Date: 2026-08-30 > 本文在 V1.0 概念稿上,依据《Xiaozhi AI 开放应用平台 Master Plan》与《Architecture Audit》修订。 > **只设计,不编码。** --- ## 0. 本文定位 本文只定义 **Device / Client**(仓库 `xiaozhi-esp32-2.2.6`)如何从「语音对话固件」演进为「可执行多种 InteractionMode 的 Device Runtime」。 不在本文展开、由 Server Plan 负责的内容: - App / Task Definition 的权威存储 - TaskInstance / StepInstance 持久化 - Scheduler / Plan / Subscription - ASR / LLM / Assessment / 家长报告 - 管理后台与开发者平台 **相对 V1.0 的关键修正:** | V1.0 | V1.1(本次) | |------|----------------| | 设备上有完整 Task Runtime | 权威 Task Runtime 在 Server;设备只有 **Task Executor** | | Phase 1 做复读 + 跟读 + 口算 + 成语接龙 | Phase 1 只做 `CHAT` + `PLAY_AUDIO`(英语复读),不破坏现有 Chat | | 新造统一 Command Protocol | **扩展现有 JSON `type` + MCP**,不新开总线 | | Skill 列表含 `math.calculate` / `llm.evaluate` | Skill 必须映射到**已有代码**;没有的标为新建 | | 设备维护 App Manager 全套生命周期 | Phase 1–2 不在固件实现 App 商店;资源同步可复用板级下载 | --- ## 1. 当前 Client 是什么(Audit 事实) 当前固件**不是** App 平台,而是: ```text Application 单例 + DeviceState 状态机 + Opus 实时双工 + MCP 工具面 ``` | 项 | 真实位置 | |----|----------| | 入口 | `main/main.cc` → `Application::Initialize` → `Application::Run` | | 主循环 | `application.cc`:FreeRTOS EventGroup,永不返回 | | 对话态 | `device_state.h`:Idle / Connecting / Listening / Speaking … | | 音频管道 | `audio/audio_service.{h,cc}`:Mic→Opus→Server;Server→Opus→Speaker | | 协议 | `protocols/protocol.{h,cc}` + `websocket_protocol` / `mqtt_protocol` | | Skill 面 | `mcp_server.cc` + 各 board `InitializeTools()` | | 资源 OTA | `ota.{h,cc}`(固件/配置)、`assets.{h,cc}`(语音模型等) | | 板级旁路 | Sening:`self.mp3.*`、`BoardMqtt`(`snlp/v1` DOWNLOAD / PLAY_MEDIA) | 完整对话链: ```text Mic → AudioService → Opus 16k/60ms → Protocol.SendAudio → Server ASR/LLM/TTS → type=tts/stt + binary Opus → AudioService decode → Speaker ``` **没有:** InteractionMode 枚举、Task/Step、可归档录音文件、核心 `play_audio`(仅 Sening 板级)、统一 Event Bus。 `ListeningMode`(auto/manual/realtime)只是拾音策略,**不是** Master Plan 的 InteractionMode。 --- ## 2. 目标 Device Runtime 目标不是把手机 OS 搬到 ESP32,而是: ```text Device Runtime ← 现有 Application 事件环升级,不是推翻 ├── Chat Runtime ← 现有 Listening ↔ Speaking,降为 Mode=CHAT └── Task Executor ← 薄执行器:收指令、跑 Skill、回 Event ├── InteractionMode 切换 └── Skill → Device Capability ``` 同一时间只运行一个 InteractionMode(Chat 与 Task 音频互斥)。 设备**不理解**业务: ```text 禁止固件出现:if (app == english) / if (task == idiom) ``` 设备只理解: ```text mode + skill + resource_ref + 完成条件(超时 / 播放结束 / VAD 静音) ``` --- ## 3. 职责边界 ```text Server(权威) 创建 TaskInstance 推进 Step 指定 InteractionMode ASR / LLM / Assessment 持久化状态与音频对象 Client(执行) 保持 Chat 热路径 按 mode 使用麦/扬声器/屏 执行 Skill(播、听、显示、下载) 上报 Event(开始/结束/失败) 缓存当前 step(断线 UI),不以本地为权威 ``` 对应 Master Plan:Device 负责执行、播放、采集、缓存、通信;Server 负责调度与评估。 --- ## 4. 分层(保留 V1.0,落到现有代码) ```text 业务层 App / Task ← 不下发到固件业务代码;只下发 JSON + 资源 交互层 Interaction Mode ← 新增;挂在 Application,与 DeviceState 正交 能力层 Skill ← 现有 MCP tools 升级为正式 Skill 目录 设备层 Device Capability ← AudioCodec / Display / Button / SD / LED ``` **DeviceState 与 InteractionMode 必须分开:** | | DeviceState(已有) | InteractionMode(新增) | |--|---------------------|-------------------------| | 回答 | 设备现在能不能听/说/升级 | 这一步用什么交互方式 | | 例子 | Idle, Listening, Speaking | CHAT, PLAY_AUDIO, FOLLOW_READ | | 谁改 | 音频通道占用 | Server 指定 / Client 执行 | 不要把 `PLAY_AUDIO` 做成一个新的 `kDeviceStatePlayingEnglishLesson`。播放时可保持 Idle 或增加**非对话**占用标记,但 **禁止**把 Task 塞进 Listening/Speaking 迁移表。 --- ## 5. Chat Runtime:保留并降级 现有 Chat **必须继续作为默认能力**。无 App、无 Task 时: ```text 唤醒 / 按键 → Connecting → Listening ↔ Speaking → Idle ``` 路径、协议 `hello/listen/abort/tts/stt/mcp`、Opus 热路径 **Phase 1–2 禁止改语义**。 降级方式: ```text InteractionMode = CHAT → 走现有 Application 对话状态机 InteractionMode = PLAY_AUDIO → 不进入 LLM → 扬声器给 play_audio → 麦可关或只用于打断/唤醒退出 任意 Mode 下唤醒词 / 退出 → 回到 CHAT 或 Idle(可配置) ``` Chat 不是 Task Runtime 的父容器,而是 Device Runtime 下与 Task Executor **并列**的子 Runtime。 --- ## 6. Task Executor(不是完整 Task Runtime) V1.0 把 Task Runtime 画在设备上。Audit 结论:编排已在 Server `ConnectionHandler`。设备侧只增加: ```text 收到 skill.execute / 板级 PLAY_MEDIA / MCP tools/call → 执行 → 上报 completed / failed → 等待下一步 ``` 设备 **不** 负责: - 创建 TaskInstance - 判断「跟读是否及格后是否重试」(可执行 Server 下发的「再播一次」) - 评分、ASR、LLM 设备 **可以** 本地判断的完成条件仅限硬件可观测: ```text 播放结束 VAD 静音超时 用户按键取消 下载完成 / 失败 ``` 状态机(执行器级,缓存用): ```text IDLE → EXECUTING → WAITING_USER? → COMPLETED | FAILED | CANCELLED ``` 权威状态仍在 Server 的 TaskInstance / StepInstance。 --- ## 7. Interaction Mode > Interaction Mode 不是业务场景(不是「英语课」),只描述硬件怎么与人交互。 ### 7.1 分阶段支持 | Mode | Phase | 现有代码基础 | 设备要做的 | |------|-------|--------------|------------| | `CHAT` | 0(已有) | Listening/Speaking | 保持 | | `PLAY_AUDIO` | 1 | Sening `self.mp3.play` / `PLAY_MEDIA`;`PauseSdCardMp3` | 抽成可调用 Skill;与 TTS 互斥 | | `FOLLOW_READ` | 2 | listen 流 + 播放 | 先播后听;听完上报,不进 LLM | | `LISTEN` | 2 | `SendStartListening` | 可单独听,不自动 Chat | | `QUIZ` / `TRANSLATION` / `GAME` | 3 | 无独立 Mode | 先用 CHAT+Skill 组合,再固化 Mode | V1.0 要求第一阶段就支持 QUIZ/TRANSLATION/GAME —— **取消**。那些是 Server 编排 + CHAT/LISTEN 即可先做,不值得先改固件状态机。 ### 7.2 PLAY_AUDIO(英语复读) ```text Server:重复 N 次的编排(权威) Client:play_audio(resource) → 播完 event → 等下一次 play 或 stop LLM = false,ASR = false ``` TTS 来时沿用现有 `PauseSdCardMp3` / `resume_sdcard_mp3_after_tts_`,**不要**另写一套混音。 ### 7.3 FOLLOW_READ(英语跟读) ```text play 标准音频 → play.completed listen(复用上行 Opus) → speech.stopped / listen.completed Server ASR + v0 文本评估 → speak 反馈(可走现有 TTS) ``` 设备不评分。 --- ## 8. Skill:只基于真实能力 Skill = 对 Device Capability 的稳定封装。第三方 App 禁止直接 GPIO/I2S。 ### 8.1 已有,可直接当作 Skill | Skill | 当前实现 | 范围 | |-------|----------|------| | `device.get_status` | `self.get_device_status` | 全局 MCP | | `audio.set_volume` | `self.audio_speaker.set_volume` | 全局 | | `screen.set_brightness` / `set_theme` | `self.screen.*` | 有屏 | | `camera.take_photo` | `self.camera.take_photo` | 有摄像头 | | `audio.play_sound` | `AudioService::PlaySound`(OGG 提示音) | 全局;不能当课程音频 | | `mp3.play/stop/list` | `self.mp3.*` | **Sening** | | `media.play` | `BoardMqtt` `PLAY_MEDIA` | **Sening snlp** | | `resource.download` | `DOWNLOAD` / Assets | Sening / Assets | | `alarm.*` | `self.alarm.*` | Sening;≠ 家长 Schedule | | `led` / 运动类 | 各 board MCP | 板相关 | ### 8.2 Phase 1 需要「提升」的 ```text play_audio 输入:SD 路径或已下载 resource_id 实现:优先复用 self.mp3.play / PLAY_MEDIA 目标:从 board 私有工具提升为 Runtime Skill(可仍 ifdef 板型) ``` 不要求所有板第一天都能 play MP3。无 SD 的板 Phase 1 可用 Server 推流(不推荐占 TTS 通道做复读循环)。 ### 8.3 Phase 2 需要新建的 | Skill | 现状 | 设计 | |-------|------|------| | `record_audio`(可归档) | **不存在**;只有流式 Opus | 优先复用 listen 流,由 Server 句级落 WAV;设备端文件录音为增强(SD `/recordings/pending`) | | `listen`(不进 Chat) | listen 消息会进入对话 | Mode=FOLLOW_READ/LISTEN 时 Client 仍发 Opus,Server **跳过 LLM** | | `wait` | 无 | 可用 Server 计时;设备只需播完/超时事件 | | `display.show` | 有屏 MCP / `SetChatMessage` | 可扩展文本/图片资源 | ### 8.4 不要做成设备 Skill 的 ```text asr.transcribe → Server llm.chat/evaluate → Server score.evaluate → Server translation.translate → Server math.calculate → Server ``` V1.0 Skill 表里的这些能力改挂 Server Plan。 --- ## 9. 音频、录音、互斥 ### 9.1 现有音频生命周期(必须保留) ```text 对话:PCM 16k → Opus 60ms 流 → 不写盘 → Server 可选转 WAV 入库 提示音:OGG PlaySound Sening 课程:SD MP3 独立播放,TTS 时 Pause ``` 热路径禁止改:`MAIN_EVENT_SEND_AUDIO`、`AudioInputTask`、`OpusCodecTask`、`FinishTtsResponse`。 ### 9.2 互斥规则 ```text 同一时刻扬声器只服务一个源:TTS | SD/MP3 | PlaySound CHAT 的 Listening/Speaking 优先于 PLAY_AUDIO(可暂停课程音频) Task 需要麦时:要么 Mode 独占 listen,要么明确与 Chat 互斥 ``` ### 9.3 跟读音频最小方案(Phase 2) **不先做**设备完整 wav 文件上传。 ```text 复用现有上行 Opus Server 按句 PCM→WAV(已有 chat_history_conf=2) 新增关联:audio_id ↔ step_instance_id ``` 离线缓存 `/recordings/pending` 放到 Phase 3,除非跟读在弱网成为阻塞。 设备侧 **没有** audio_id;只有 `session_id`(连接级)。audio_id 由 Server 生成后回传(可选)。 --- ## 10. App 资源 vs Firmware OTA 原则保留 V1.0 / Master Plan: ```text OTA = 驱动、协议、Audio Engine、Runtime、Skill 宿主 App Sync = manifest、task JSON、MP3、图片、配置 ``` App **原则上不 OTA**。但下列能力必须在 Firmware 里(可一次 OTA 补齐,之后新 App 不再改 C++): ```text Codec / VAD / 唤醒 WS 或 MQTT+UDP 协议栈 MCP / Skill 宿主 SD 文件系统 HTTP 下载(或等价) 音频互斥 InteractionMode 开关 (Phase 2)非 Chat 的 listen 语义 ``` ### 10.1 SD 布局(目标,Phase 2+ 落地) ```text /sdcard/ apps// # manifest + tasks JSON resources// # mp3 / 图 recordings/pending/ # Phase 3 cache/ ``` Sening 当前下载路径偏 `/data/courses/...`(smartlearning)。演进时 **映射到统一布局**,不要强迫立即改终端已有文件。 ### 10.2 双 MQTT 收敛 当前存在两套: | 通道 | 用途 | |------|------| | Xiaozhi `Protocol` | 对话 JSON + Opus | | Sening `snlp/v1` | 心跳、DOWNLOAD、PLAY_MEDIA、ack/progress | **Phase 1:** 允许并存;复读优先走已能工作的 MCP/`PLAY_MEDIA`。 **Phase 2–3:** 资源同步与播放控制逐步收到 Xiaozhi JSON `type` 或 MCP,snlp 降为板级实现细节,避免业务云长期分叉。 **禁止 Phase 1 合并两套 MQTT。** --- ## 11. 协议:扩展,不替换 现有设备→云:`hello / listen / abort / iot / mcp / ping / server` 现有云→设备:`hello / stt / tts / llm / mcp / system / alert / custom` ### Phase 1 不强制新 `type`。复用: ```text type=mcp tools/call self.mp3.play | stop 或 snlp PLAY_MEDIA ``` ### Phase 2 新增(与 tts/stt/mcp 并列) 下行: ```text task.start / task.stop / task.pause / task.resume step.start skill.execute mode.set # CHAT | PLAY_AUDIO | FOLLOW_READ | LISTEN ``` 上行: ```text task.started / task.completed / task.failed step.started / step.completed / step.failed skill.completed / skill.failed audio.play.completed speech.started / speech.stopped # 可从现有 VAD 事件导出 ``` 载荷必须带:`session_id`;Phase 2 起带 `task_instance_id` / `step_id`。 **不要**新建第四套 EventBus。Sening `rpt/event` 可作实现,但语义应对齐上表。 插入点:`Application` 中 `protocol_->OnIncomingJson`(`application.cc` 约 647–778 行),`unknown type` 分支旁增加 `task` / `mode`。 --- ## 12. Event 与主循环插入点 现有机制:`MAIN_EVENT_*` + 协议回调 + MCP JSON-RPC +(Sening)rpt/ack。 Task Executor 挂载顺序(侵入性从低到高): 1. **MCP `tools/call`** — Phase 1 首选,Chat 路径不动 2. **`Application::Schedule`** — 线程安全推进 UI / 非音频副作用 3. **OnIncomingJson 新 type** — Phase 2 4. **禁止**把 Task 状态写入 `DeviceStateMachine::IsValidTransition` 的 Listening/Speaking 边 按键:今日绑定 `ToggleChatState` / Start/Stop Listening。Phase 2 起允许「取消当前 Task / 跳过」而不拆掉 Chat 按键。 --- ## 13. 本地状态(薄) 设备只持久化必要项(NVS / 内存): ```text activeMode activeTaskInstanceId # 缓存,权威在 Server activeStepId # 缓存 sdcard play 状态 # 已有 ``` 不在设备保存完整 Step 历史、评分、ASR 文本。 App 生命周期 V1.0 的 INSTALL/LAUNCH/ACTIVE 全套 **Phase 3** 再做。Phase 1 甚至不需要 `activeAppId`。 --- ## 14. 分阶段(Client) 与 Server Plan 阶段编号对齐。 ### Phase 1 — 双模式证明(必须不破坏 Chat) 目标:英语复读 N 遍。 Client 工作: - 文档化并复用 `self.mp3.play` / `PLAY_MEDIA` + `PauseSdCardMp3` - 约定:播放期间唤醒/按键可回到 Chat(现有行为优先) - **不改** Opus 热路径、DeviceState 迁移、hello/listen/tts 语义 - 不实现 Task 状态机、App Manager、跟读录音文件 验收: 1. 无 Task 时 Chat 与现网一致 2. 能循环播放指定 SD/已下载音频 N 次 3. TTS/唤醒仍能打断或暂停播放 ### Phase 2 — Task Executor + 跟读采集 - `mode.set` + `type=task` / `skill.execute` - FOLLOW_READ:播完再 listen,Server 跳过 LLM - 上报 play/listen 完成事件(带 task/step id) - 录音最小方案:流式上传 + Server 归档关联,不强制设备 wav 文件 验收:一次跟读 Step 能在 Server 看到 audio_id + asr_text。 ### Phase 3 — Runtime 平台化 - 核心 `play_audio` / 下载进非 Sening 抽象(或明确仅支持 SD 板) - App 资源目录与增量 sync - snlp 与 Xiaozhi 协议收敛 - 可选:本地 pending 录音、QUIZ/GAME 作为 Mode 而不是硬编码业务 - 按键可绑定 Task 取消 --- ## 15. 禁止事项(Client) 1. 重写 AudioService 热路径或「为了 Task 换一套编解码」 2. 在固件写英语/口算/成语业务分支 3. 用 OTA 发课程 App 4. Phase 1 合并两套 MQTT 5. 把 Task 塞进 Listening/Speaking 6. 设备上做 ASR/评分/情绪分析 7. 为 Demo 写 `EnglishRepeatManager` 这种一次性类(可用薄 `TaskExecutor` + Skill) 8. 引入通用 EventBus / Redux 9. 修改现有 Chat 协议字段含义 --- ## 16. AI Coding 约束(保留并收紧 V1.0) 1. 先读现有 `Application` / `AudioService` / `Protocol` / `mcp_server`,再改。 2. 无 App/Task 时 Chat 必须可用。 3. 业务不下沉到 Client。 4. 配置/JSON 驱动 InteractionMode 与 Skill,不写死 App。 5. 最小改动:能 MCP 解决的 Phase 1 不改协议。 --- ## 17. 与 Server Plan 的契约 Client 保证: - CHAT 行为向后兼容 - 能执行 `play_audio`(至少 Sening 路径) - Phase 2 能按 mode 听而不自动当 Chat 回复引擎 - 事件可关联 `session_id`(及后续 instance id) Client 要求 Server: - 权威推进 Step,不要指望设备记住整张 Workflow - PLAY_AUDIO / FOLLOW_READ 不要误入 `ConnectionHandler.chat()` - 音频对象与 step 关联在云端完成 - App JSON 与资源 URL 由云下发,不要求固件内置课程 下一份可执行文档应是 **Phase 1 Coding Plan(Client 切片)**,而不是继续加 Mode。